package com.weilus.gateway;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.PathContainer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;
import reactor.core.publisher.Mono;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * spring:
 *   cloud:
 *     gateway:
 *       routes:
 *         - id: flux-call
 *           filters:
 *           - name: Authentication
 *             args:
 *             - GET /test/**=authenticated()               #需要认证才能访问
 *             - PUT /test/add=auth2.hasScope('user-info')  #认证且指定域权限才能访问
 *             - /test/other=hasRole('aa,bb')               #认证且指定权限才能访问
 */
@Component
@Slf4j
public class AuthenticationGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthenticationGatewayFilterFactory.Config> {
    public static final OAuth2Authentication NULL_AUTHENTICATION = new OAuth2Authentication(new OAuth2Request(),null);
    private ReactiveRedisTemplate<String,OAuth2Authentication> redisTemplate;

    public AuthenticationGatewayFilterFactory(ReactiveRedisConnectionFactory factory){
        super(Config.class);
        RedisSerializer jdkSerializer = new JdkSerializationRedisSerializer();
        RedisSerializationContext.RedisSerializationContextBuilder<String, ?> builder = RedisSerializationContext.newSerializationContext(new StringRedisSerializer());
        RedisSerializationContext context = builder.value(jdkSerializer).build();
        this.redisTemplate = new ReactiveRedisTemplate<>(factory, context);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            PathContainer path = PathContainer.parsePath(exchange.getRequest().getURI().getRawPath());
            HttpMethod method = exchange.getRequest().getMethod();
            String token  = Optional.ofNullable(exchange.getRequest().getHeaders().getFirst("token")).orElse(exchange.getRequest().getQueryParams().getFirst("token"));
            Set<AuthMatcher> matches = config.getAuthMatchers()
                    .stream()
                    .filter(m->m.getPathPattern().matches(path) && (m.getMethod() == null || m.getMethod() == method))
                    .collect(Collectors.toSet());
            if(!CollectionUtils.isEmpty(matches)){
                if(StringUtils.isEmpty(token))return noPermit(exchange);
                return redisTemplate.opsForValue().get("auth:"+token)
                        .defaultIfEmpty(NULL_AUTHENTICATION)
                        .flatMap(auth-> access(auth, matches)?chain.filter(exchange):noPermit(exchange));
            } else {
                return chain.filter(exchange);
            }
        };
    }

    public Mono<Void> noPermit(ServerWebExchange exchange){
        ServerWebExchangeUtils.setResponseStatus(exchange,HttpStatus.FORBIDDEN);
        return exchange.getResponse().setComplete();
    }

    public boolean access(OAuth2Authentication authentication,Set<AuthMatcher> matchs){
        for(AuthMatcher m : matchs){
            if(m.isRequiredAuthenticated() && !authentication.isAuthenticated()){
                log.debug("{} {} Required Authenticated! {}",m.getMethod(),m.getPathPattern(),authentication);
                return false;
            }
            if(!CollectionUtils.isEmpty(m.getRequiredScopes())){
                Set<String> grantedScopes = authentication.getOAuth2Request().getScope();
                boolean access = !CollectionUtils.isEmpty(grantedScopes) &&
                        grantedScopes.stream().anyMatch(grantedScope-> m.getRequiredScopes().contains(grantedScope));
                if(!access){
                    log.debug("{} {} Required Scope: {}",m.getMethod(),m.getPathPattern(),m.getRequiredScopes());
                    return false;
                }
            }
            if(!CollectionUtils.isEmpty(m.getRequiredAuthorities())){
                Collection<GrantedAuthority> grantedRoles = authentication.getAuthorities();
                boolean access = !CollectionUtils.isEmpty(grantedRoles) &&
                        grantedRoles.stream().anyMatch(grantedRole-> m.getRequiredAuthorities().contains(grantedRole));
                if(!access){
                    log.debug("{} {} Required Authorities: {}",m.getMethod(),m.getPathPattern(),m.getRequiredAuthorities());
                    return false;
                }
            }
        }
        return true;
    }

    public static class Config extends ArrayList<String>{
        private Set<AuthMatcher> authMatchers = new HashSet<>();
        @Override
        public boolean add(String o) {
            authMatchers.add(new AuthMatcher(o));
            return super.add(o);
        }
        public Set<AuthMatcher> getAuthMatchers() {
            return authMatchers;
        }
    }
    @Data
    public static class AuthMatcher{
        public final static Pattern scopePattern = Pattern.compile("^auth2.hasScope\\('(.*?)'\\)$");
        public final static Pattern rolePattern = Pattern.compile("^hasRole\\('(.*?)'\\)$");
        public final static PathPatternParser pathPatternParser = new PathPatternParser();

        private PathPattern pathPattern;
        private HttpMethod method;
        private boolean requiredAuthenticated;
        private Set<String> requiredScopes;
        private Set<GrantedAuthority> requiredAuthorities;

        public AuthMatcher(String config){
            String[] arr = StringUtils.delimitedListToStringArray(config,"=");
            String pathConfig = arr[0],authConfig = arr[1];
            String[] pathArr = StringUtils.delimitedListToStringArray(pathConfig," ");
            this.pathPattern = pathPatternParser.parse(pathArr.length == 1 ? pathArr[0] : pathArr[1]);
            this.method = HttpMethod.resolve(pathArr.length == 1 ? null : pathArr[0]);
            this.requiredAuthenticated = authConfig.equals("authenticated()");
            Matcher scopeMatcher = scopePattern.matcher(authConfig);
            Matcher roleMatcher = rolePattern.matcher(authConfig);
            if(scopeMatcher.find()){
                this.requiredAuthenticated = true;
                this.requiredScopes = StringUtils.commaDelimitedListToSet(scopeMatcher.group(1));
            }else if(roleMatcher.find()){
                this.requiredAuthenticated = true;
                this.requiredAuthorities = StringUtils.commaDelimitedListToSet(scopeMatcher.group(1)).stream()
                        .distinct()
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toSet());
            }
        }

        @Override
        public String toString() {
            return "{" +
                    "method=" + method +
                    ",pathPattern=" + pathPattern +
                    ", requiredAuthenticated=" + requiredAuthenticated +
                    ", requiredScopes=" + requiredScopes +
                    ", requiredAuthorities=" + requiredAuthorities +
                    '}';
        }
    }



}
